package link

import (
	"context"
	"errors"
	"fmt"
	"gitee.com/chunanyong/zorm"
	"gitee.com/xinshortlink/internal/constants"
	"gitee.com/xinshortlink/internal/dal/db/dao"
	"gitee.com/xinshortlink/internal/dal/db/model"
	"gitee.com/xinshortlink/internal/dto"
	"gitee.com/xinshortlink/internal/dto/resp/bizerr"
	"gitee.com/xinshortlink/internal/pkg/conf"
	"gitee.com/xinshortlink/internal/pkg/idgenerater"
	"gitee.com/xinshortlink/internal/pkg/token"
	"gitee.com/xinshortlink/internal/pkg/utils"
	"gitee.com/xinshortlink/internal/pkg/utils/hotspotkey"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"golang.org/x/sync/singleflight"
	"hash"
	"hash/fnv"
	"math"
	"strconv"
	"time"
)

const linkCacheKey = "link_cache:"        // 短链接缓存key
const linkCacheNilKey = "link_nil_cache:" // 短链接不存在缓存key

type Service struct {
	logger        *zap.Logger
	dao           *dao.LinkDao
	shortRouteDao *dao.ShortRouteDao
	loginUtil     *token.LoginAccountUtil
	g             *idgenerater.IdGenerate
	fnv32         hash.Hash32
	domain        string
	rdb           *redis.Client
	sfg           singleflight.Group
	hotspot       *hotspotkey.HotSpotDetector
}

func NewService(logger *zap.Logger, dao *dao.LinkDao, loginUtil *token.LoginAccountUtil,
	g *idgenerater.IdGenerate, conf *conf.Conf, rdb *redis.Client, shortRouteDao *dao.ShortRouteDao) (*Service, error) {

	s := &Service{
		logger:        logger,
		dao:           dao,
		loginUtil:     loginUtil,
		g:             g,
		fnv32:         fnv.New32a(),
		domain:        conf.Short.Domain,
		rdb:           rdb,
		hotspot:       hotspotkey.NewHotSpotDetector(logger),
		shortRouteDao: shortRouteDao,
	}

	// 注册热点数据回调
	s.hotspot.RegisterCallback(func(ctx context.Context, shortId string) (value string, err error) {
		logger.Info(">>>触发热点")

		key := linkCacheKey + shortId
		originUrl, err := s.rdb.Get(ctx, key).Result()
		if err != nil {
			return "", err
		}

		logger.Info(">>>触发热点", zap.String("originUrl", originUrl))
		return originUrl, nil
	})

	return s, nil
}

func (s *Service) Links(ctx context.Context, req *dto.FindLinkDTO) (*dto.PageResp[*dto.LinkDTO], error) {
	page, list, err := s.dao.FindLinks(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: Links fail %w", err)
	}

	slices := make([]*dto.LinkDTO, len(list))
	result := &dto.PageResp[*dto.LinkDTO]{
		Records:    slices,
		TotalCount: page.TotalCount,
		PageCount:  page.PageCount,
		FirstPage:  page.FirstPage,
		HasPrev:    page.HasPrev,
		HasNext:    page.HasNext,
		LastPage:   page.LastPage,
	}

	for i, l := range list {
		result.Records[i] = &dto.LinkDTO{
			Id:            l.Id,
			ShortId:       l.ShortId,
			ShortUrl:      l.ShortUrl,
			OriginUrl:     utils.ReverseString(l.OriginUrl),
			ClickNum:      l.ClickNum,
			Gid:           l.Gid,
			Favicon:       l.Favicon,
			EnableStatus:  l.EnableStatus,
			ValidDateType: l.ValidDateType,
			ValidDate:     l.ValidDate,
			Info:          l.Info,
			CreateTime:    l.CreateTime,
			UpdateTime:    l.UpdateTime,
			DelTime:       l.DelTime,
			DelState:      l.DelState,
		}
	}

	return result, nil
}

func (s *Service) Create(ctx context.Context, req *dto.CreateLinkReq) (err error) {
	validDate := time.Time{}
	if req.ValidDateType != constants.ForeverValid {
		validDate, err = time.Parse(time.DateTime, req.ValidDate)
		if err != nil {
			return fmt.Errorf("SERVICE: Create Link fail %w", err)
		}
	}

	shortId, err := s.generateShort(ctx)
	if err != nil {
		return fmt.Errorf("SERVICE: Create Link fail %w", err)
	}

	shortUrl := s.domain + "/r/" + shortId

	originUrl := utils.ReverseString(req.OriginUrl)

	_, err = zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {
		link := &model.LinkStruct{
			ShortId:       shortId,
			ShortUrl:      shortUrl,
			OriginUrl:     originUrl,
			Gid:           req.Gid,
			Favicon:       "",
			EnableStatus:  req.EnableStatus,
			ValidDateType: req.ValidDateType,
			ValidDate:     validDate,
			Info:          req.Info,
			CreateTime:    time.Now(),
			UpdateTime:    time.Now(),
			DelState:      constants.NO,
		}
		err = s.dao.Create(ctx, link) // 创建短链接

		if err != nil {
			return nil, fmt.Errorf("SERVICE: Create Link fail %w", err)
		}

		err = s.shortRouteDao.Create(ctx, req.Gid, shortId)
		if err != nil {
			return nil, fmt.Errorf("SERVICE: Create Link Route fail %w", err)
		}

		// 清空空跳转缓存
		keyNil := linkCacheNilKey + shortId
		nilExists, err := s.rdb.Exists(ctx, keyNil).Result()
		if err != nil {
			if !errors.Is(err, redis.Nil) {
				return nil, fmt.Errorf("SERVICE: Create Link fail %w", bizerr.New(bizerr.Cache, fmt.Sprintf("查询缓存失败 key=%s\n", keyNil), err))
			}
		}

		if nilExists == 1 {
			_, err := s.rdb.Del(ctx, keyNil).Result()
			if err != nil {
				return nil, fmt.Errorf("SERVICE: Create Link fail %w", bizerr.New(bizerr.Cache, fmt.Sprintf("删除缓存失败 key=%s\n", keyNil), err))
			}
		}

		// 增加布隆过滤器
		_, err = s.rdb.BFAdd(ctx, constants.CreateLinkBFKey, shortId).Result()
		if err != nil {
			return nil, fmt.Errorf("SERVICE: Create Link fail %w", bizerr.New(bizerr.Cache, fmt.Sprintf("增加布隆过滤器失败 key=%s\n", constants.CreateLinkBFKey), err))
		}

		expiration := s.generateValidDate(link)

		// 增加短链接缓存
		key := linkCacheKey + shortId
		_, err = s.rdb.Set(ctx, key, req.OriginUrl, expiration).Result()

		if err != nil {
			return nil, fmt.Errorf("SERVICE: Create Link fail %w", bizerr.New(bizerr.Cache, fmt.Sprintf("增加缓存失败 key=%s\n", key), err))
		}

		return nil, nil
	})

	return err
}

func (s *Service) Del(ctx context.Context, id, gid int64) error {
	_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {
		err := s.dao.Del(ctx, id, gid)
		if err != nil {
			return nil, err
		}

		return nil, nil
	})

	if err != nil {
		return fmt.Errorf("SERVICE: Del Link fail %w", err)
	}
	return nil
}

// FindRedirectUrl 根据短链接找到原始链接
func (s *Service) FindRedirectUrl(ctx context.Context, shortId string) (string, error) {
	if originUrl, b := s.hotspot.GetHotspot(ctx, shortId); b {
		return originUrl, nil
	}

	key := linkCacheKey + shortId
	originUrl, err := s.rdb.Get(ctx, key).Result() // 查询缓存是否存在
	if err != nil {
		if !errors.Is(err, redis.Nil) {
			return "", fmt.Errorf("SERVICE: FindRedirectUrl fail %w", bizerr.New(bizerr.Cache, fmt.Sprintf("查询缓存失败 key=%s\n", key), err))
		}
	}

	if originUrl != "" {
		//s.logger.Info(">>>触发缓存")
		return originUrl, nil
	}

	ch := s.sfg.DoChan(shortId, func() (interface{}, error) {
		originUrl, err = s.rdb.Get(ctx, key).Result() // 查询缓存是否存在
		if err != nil {
			if !errors.Is(err, redis.Nil) {
				return "", bizerr.New(bizerr.Cache, fmt.Sprintf("查询缓存失败 key=%s\n", key), err)
			}
		}

		if originUrl != "" {
			return originUrl, nil
		}

		exists, err := s.rdb.BFExists(ctx, constants.CreateLinkBFKey, shortId).Result() // 布隆过滤器判断是否存在
		if err != nil {
			return "", bizerr.New(bizerr.Cache, fmt.Sprintf("查询布隆过滤器失败 key=%s\n", key), err)
		}

		if !exists {
			return "", bizerr.ErrLinkNotFound
		}

		keyNil := linkCacheNilKey + shortId
		nilExists, err := s.rdb.Exists(ctx, keyNil).Result() // 防止缓存穿透。如果key已被缓存为空跳转。直接返回短链接不存在。
		if err != nil {
			if !errors.Is(err, redis.Nil) {
				return "", bizerr.ErrLinkNotFound
			}
			return "", bizerr.New(bizerr.Cache, fmt.Sprintf("查询缓存失败 key=%s\n", keyNil), err)
		}

		if nilExists == 1 {
			return "", bizerr.ErrLinkNotFound
		}

		link, err := s.FindOriginUrl(ctx, shortId)
		if err != nil {
			return "", fmt.Errorf("SERVICE: FindRedirectUrl fail %w", err)
		}

		expiration := s.generateValidDate(link)

		// 缓存
		_, err = s.rdb.Set(ctx, key, link.OriginUrl, expiration).Result()
		if err != nil {
			return "", bizerr.New(bizerr.Cache, fmt.Sprintf("设置缓存失败 key=%s\n", key), err)
		}

		return link.OriginUrl, nil
	})

	ctx, cancelFunc := context.WithTimeout(ctx, time.Second)
	defer cancelFunc()

	select {
	case <-ctx.Done():
		return "", fmt.Errorf("SERVICE: FindRedirectUrl fail %w", bizerr.New(bizerr.ServerTimeout, "查询超时", ctx.Err()))
	case r := <-ch:
		if r.Err != nil {
			return "", fmt.Errorf("SERVICE: FindRedirectUrl fail %w", bizerr.New(bizerr.Server, "合并请求出错", r.Err))
		}
		if v, ok := r.Val.(string); ok {
			return v, nil
		}
	}

	return "", bizerr.ErrLinkNotFound
}

func (s *Service) FindOriginUrl(ctx context.Context, shortId string) (*model.LinkStruct, error) {
	gid, err := s.shortRouteDao.FindGidByShortId(ctx, shortId)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: FindOriginUrl fail %w", err)
	}

	if gid == 0 {
		return nil, nil
	}

	link, err := s.dao.FindLinkByShortId(ctx, shortId, gid)
	if err != nil {
		return nil, fmt.Errorf("SERVICE: FindOriginUrl fail %w", err)
	}

	if link == nil {
		return nil, bizerr.ErrLinkNotFound
	}

	if link.DelState == constants.YES {
		return nil, bizerr.ErrLinkNotFound
	}

	if link.EnableStatus == constants.NO {
		return nil, bizerr.ErrLinkEnableStatus
	}

	if link.ValidDateType != constants.ForeverValid { // 自定义时间
		if link.ValidDate.Before(time.Now()) {
			return nil, bizerr.ErrLinkValidDate
		}
	}

	if link.OriginUrl == "" {
		return nil, bizerr.ErrLinkNotFound
	}

	link.OriginUrl = utils.ReverseString(link.OriginUrl)

	return link, nil
}

const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

func (s *Service) generateShort(ctx context.Context) (string, error) {
	gNum := 0 // 生成次数
	maxNum := 10
	for {
		if gNum >= maxNum {
			return "", bizerr.ErrLinkGenerate
		}

		id, err := s.g.GetId() // 雪花算法
		if err != nil {
			s.logger.Error("generate id error", zap.Error(err))
			gNum++
			continue
		}

		_, err = s.fnv32.Write([]byte(strconv.FormatInt(id, 10)))
		if err != nil {
			s.logger.Error("fnv32 write error", zap.Error(err))
			gNum++
			continue
		}

		num := s.fnv32.Sum32()

		if num < 0 {
			num = math.MaxInt16 - num
		}

		var result []byte
		for num > 0 {
			remainder := num % 62
			result = append(result, base62Chars[remainder])
			num = num / 62
		}

		// 反转结果
		for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
			result[i], result[j] = result[j], result[i]
		}

		shortId := string(result)

		// 改为 Redis 布隆过滤器
		b, err := s.rdb.BFExists(ctx, constants.CreateLinkBFKey, shortId).Result()
		if err != nil {
			s.logger.Error("create bf error", zap.Error(err))
			gNum++
			continue
		}

		if b {
			s.logger.Error("shortId exist error", zap.Error(err))
			gNum++
			continue
		}

		// FIXME 性能优化点 直接使用查询数据库性能挺差的。
		//count, err := s.dao.FindLinkCountByShortId(ctx, shortId)
		//if err != nil {
		//	s.logger.Error("dao find link error", zap.Error(err))
		//	gNum++
		//	continue
		//}
		//if count > 0 {
		//	s.logger.Error("shortId exist error", zap.Error(err))
		//	gNum++
		//	continue
		//}

		return shortId, nil
	}

}

func (s *Service) UpdateStatus(ctx context.Context, id, gid int64) error {
	_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {
		link, err := s.dao.FindLinkById(ctx, id, gid)
		if err != nil {
			return nil, err
		}

		if link == nil {
			return nil, bizerr.ErrLinkNotFound
		}

		// 1：未启用 2：启用
		var status int8
		if link.EnableStatus == constants.YES {
			status = constants.NO
		} else if link.EnableStatus == constants.NO {
			status = constants.YES
		}

		err = s.dao.UpdateStatus(ctx, id, gid, status)

		key := linkCacheKey + link.ShortId
		expiration := s.generateValidDate(link)
		if status == constants.YES {
			originUrl := utils.ReverseString(link.OriginUrl)
			_, err = s.rdb.Set(ctx, key, originUrl, expiration).Result()
			if err != nil {
				return nil, err
			}
		} else if status == constants.NO {
			_, err = s.rdb.Del(ctx, key).Result()
			if err != nil {
				return nil, err
			}
		}

		return nil, err
	})
	return err
}

func (s *Service) generateValidDate(link *model.LinkStruct) time.Duration {
	var expiration time.Duration
	if link.ValidDateType == constants.ForeverValid {
		expiration = 30 * 24 * time.Hour
	} else {
		expiration = link.ValidDate.Sub(time.Now())
	}
	return expiration
}
